using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Linq.Expressions;

namespace SmartClientPlatform.RemoteClientBase
{
    internal class Scope<T> : IScope<T>
    {
        private readonly DataContext context;
        private readonly IEnumerable<T> enumerable;
        private readonly DataLoadOptions dataLoadOptions;
        private readonly ScopeItem<T> scopeItem;

        public Scope(DataContext context, IEnumerable<T> enumerable)
        {
            this.context = context;
            this.enumerable = enumerable;
            dataLoadOptions = new DataLoadOptions();
            scopeItem = new ScopeItem<T>(this);
        }

        private void AddExpression<T1, T2>(Expression<Func<T1, T2>> expression)
        {
            dataLoadOptions.LoadWith(expression);
        }

        private class ScopeItem<T2> : IScopeItem<T2>
        {
            private readonly Scope<T> parent;

            public ScopeItem(Scope<T> parent)
            {
                this.parent = parent;
            }

            private readonly Queue<IItem> refItems = new Queue<IItem>();
            public IScopeItem<TProperty> Rel<TProperty>(Expression<Func<T2, TProperty>> expression)
            {
                parent.AddExpression(expression);
                var scopeItem = new ScopeItem<TProperty>(parent);
                refItems.Enqueue(new RefItem<TProperty>(expression, scopeItem));
                return scopeItem;
            }

            private interface IItem
            {
                void CopyObject(T2 linqObject, DataContainer<T2> data, DtoInentityMap dtoInentityMap);
            }

            private class RefItem<TProperty> : IItem
            {
                private readonly Expression<Func<T2, TProperty>> expression;
                private readonly ScopeItem<TProperty> scopeItem;

                public RefItem(Expression<Func<T2, TProperty>> expression, ScopeItem<TProperty> scopeItem)
                {
                    this.expression = expression;
                    this.scopeItem = scopeItem;
                }

                public void CopyObject(T2 linqObject, DataContainer<T2> data, DtoInentityMap dtoInentityMap)
                {
                    var relatedLinqObject = linqObject.GetValue(expression);
                    var relatedDto = dtoInentityMap.CopyData(relatedLinqObject);
                    data.SetRef(expression, relatedDto);
                    scopeItem.CopyRelatedObjects(relatedLinqObject, relatedDto, dtoInentityMap);
                }
            }

            internal void CopyRelatedObjects(T2 linqObject, DataContainer<T2> data, DtoInentityMap dtoInentityMap)
            {
                foreach (var refItem in refItems)
                {
                    refItem.CopyObject(linqObject, data, dtoInentityMap);
                }
            }

            public IScopeItem<TSetItem> Rel<TSetItem>(Expression<Func<T2, EntitySet<TSetItem>>> expression)
                where TSetItem : class
            {
                parent.AddExpression(expression);
                var scopeItem = new ScopeItem<TSetItem>(parent);
                refItems.Enqueue(new SetItem<TSetItem>(expression, scopeItem));
                return scopeItem;
            }

            public IScopeItem<T2> Field<TProperty>(Expression<Func<T2, TProperty>> expression)
            {
                throw new NotImplementedException();
            }

            private class SetItem<TProperty> : IItem where TProperty : class
            {
                private readonly Expression<Func<T2, EntitySet<TProperty>>> expression;
                private readonly ScopeItem<TProperty> scopeItem;

                public SetItem(Expression<Func<T2, EntitySet<TProperty>>> expression, 
                    ScopeItem<TProperty> scopeItem)
                {
                    this.expression = expression;
                    this.scopeItem = scopeItem;
                }

                public void CopyObject(T2 linqObject, DataContainer<T2> data, DtoInentityMap dtoInentityMap)
                {
                    foreach (var relatedLinqObject in linqObject.GetValue(expression))
                    {
                        var relatedDto = dtoInentityMap.CopyData(relatedLinqObject);
                        relatedDto.SetRef(expression.GetRefPropertyName(), data);
                        scopeItem.CopyRelatedObjects(relatedLinqObject, relatedDto, dtoInentityMap);
                    }
                }
            }
        }

        public IScopeItem<TProperty> Rel<TProperty>(Expression<Func<T, TProperty>> expression)
        {
            return scopeItem.Rel(expression);
        }

        public IScopeItem<TSetItem> Rel<TSetItem>(Expression<Func<T, EntitySet<TSetItem>>> expression)
            where TSetItem : class
        {
            return scopeItem.Rel(expression);
        }

        public IScopeItem<T> Field<TProperty>(Expression<Func<T, TProperty>> expression)
        {
            throw new NotImplementedException();
        }

        public IEnumerable<T2> GetDto<T2>(Func<T2> func) where T2 : DtoBase<T>
        {
            context.LoadOptions = dataLoadOptions;
            var dtoInentityMap = new DtoInentityMap();
            foreach (var linqObject in enumerable)
            {
                var dto = dtoInentityMap.CopyData(linqObject);
                scopeItem.CopyRelatedObjects(linqObject, dto, dtoInentityMap);
                yield return dto.Wrap(func);
            }
        }

        public List<T2> GetDtoList<T2>(Func<T2> func) where T2 : DtoBase<T>
        {
            return GetDto(func).ToList();
        }

        private class DtoInentityMap
        {
            private readonly Dictionary<object, object> dictionary = new Dictionary<object, object>();
            public DataContainer<T2> CopyData<T2>(T2 linqObject)
            {
                object value;
                if (dictionary.TryGetValue(linqObject, out value))
                {
                    return (DataContainer<T2>)value;
                }
                else
                {
                    var dtoBase = new DataContainer<T2>();
                    foreach (var propertiesMame in MetaDataProvider.GetPropertiesMames<T2>())
                    {
                        dtoBase.SetValue(propertiesMame, linqObject.GetValue(propertiesMame));
                    }
                    dictionary.Add(linqObject, dtoBase);
                    return dtoBase;
                }
            }
        }
    }
}